/**
 * Graphology Dijkstra Shortest Path
 * ==================================
 *
 * Graphology implementation of Dijkstra shortest path for weighted graphs.
 */
var isGraph = require('graphology-utils/is-graph');
var createEdgeWeightGetter =
  require('graphology-utils/getters').createEdgeWeightGetter;
var Heap = require('mnemonist/heap');

/**
 * Defaults & helpers.
 */
var DEFAULT_WEIGHT_ATTRIBUTE = 'weight';

function DIJKSTRA_HEAP_COMPARATOR(a, b) {
  if (a[0] > b[0]) return 1;
  if (a[0] < b[0]) return -1;

  if (a[1] > b[1]) return 1;
  if (a[1] < b[1]) return -1;

  if (a[2] > b[2]) return 1;
  if (a[2] < b[2]) return -1;

  return 0;
}

function BRANDES_DIJKSTRA_HEAP_COMPARATOR(a, b) {
  if (a[0] > b[0]) return 1;
  if (a[0] < b[0]) return -1;

  if (a[1] > b[1]) return 1;
  if (a[1] < b[1]) return -1;

  if (a[2] > b[2]) return 1;
  if (a[2] < b[2]) return -1;

  if (a[3] > b[3]) return 1;
  if (a[3] < b[3]) return -1;

  return 0;
}

/**
 * Bidirectional Dijkstra shortest path between source & target node abstract.
 *
 * Note that this implementation was basically copied from networkx.
 *
 * @param  {Graph}  graph         - The graphology instance.
 * @param  {string} source        - Source node.
 * @param  {string} target        - Target node.
 * @param  {string} getEdgeWeight - Name of the weight attribute or getter function.
 * @param  {array}                - The found path if any and its cost.
 */
function abstractBidirectionalDijkstra(graph, source, target, getEdgeWeight) {
  source = '' + source;
  target = '' + target;

  // Sanity checks
  if (!isGraph(graph))
    throw new Error(
      'graphology-shortest-path/dijkstra: invalid graphology instance.'
    );

  if (source && !graph.hasNode(source))
    throw new Error(
      'graphology-shortest-path/dijkstra: the "' +
        source +
        '" source node does not exist in the given graph.'
    );

  if (target && !graph.hasNode(target))
    throw new Error(
      'graphology-shortest-path/dijkstra: the "' +
        target +
        '" target node does not exist in the given graph.'
    );

  getEdgeWeight = createEdgeWeightGetter(
    getEdgeWeight || DEFAULT_WEIGHT_ATTRIBUTE
  ).fromMinimalEntry;

  if (source === target) return [0, [source]];

  var distances = [{}, {}],
    paths = [{}, {}],
    fringe = [
      new Heap(DIJKSTRA_HEAP_COMPARATOR),
      new Heap(DIJKSTRA_HEAP_COMPARATOR)
    ],
    seen = [{}, {}];

  paths[0][source] = [source];
  paths[1][target] = [target];

  seen[0][source] = 0;
  seen[1][target] = 0;

  var finalPath = [],
    finalDistance = Infinity;

  var count = 0,
    dir = 1,
    item,
    edges,
    cost,
    d,
    v,
    u,
    e,
    i,
    l;

  fringe[0].push([0, count++, source]);
  fringe[1].push([0, count++, target]);

  while (fringe[0].size && fringe[1].size) {
    // Swapping direction
    dir = 1 - dir;

    item = fringe[dir].pop();
    d = item[0];
    v = item[2];

    if (v in distances[dir]) continue;

    distances[dir][v] = d;

    // Shortest path is found?
    if (v in distances[1 - dir]) return [finalDistance, finalPath];

    edges = dir === 1 ? graph.inboundEdges(v) : graph.outboundEdges(v);

    for (i = 0, l = edges.length; i < l; i++) {
      e = edges[i];
      u = graph.opposite(v, e);
      cost = distances[dir][v] + getEdgeWeight(e, graph.getEdgeAttributes(e));

      if (u in distances[dir] && cost < distances[dir][u]) {
        throw Error(
          'graphology-shortest-path/dijkstra: contradictory paths found. Do some of your edges have a negative weight?'
        );
      } else if (!(u in seen[dir]) || cost < seen[dir][u]) {
        seen[dir][u] = cost;
        fringe[dir].push([cost, count++, u]);
        paths[dir][u] = paths[dir][v].concat(u);

        if (u in seen[0] && u in seen[1]) {
          d = seen[0][u] + seen[1][u];

          if (finalPath.length === 0 || finalDistance > d) {
            finalDistance = d;
            finalPath = paths[0][u].concat(paths[1][u].slice(0, -1).reverse());
          }
        }
      }
    }
  }

  // No path was found
  return [Infinity, null];
}

/**
 * Multisource Dijkstra shortest path abstract function. This function is the
 * basis of the algorithm that every other will use.
 *
 * Note that this implementation was basically copied from networkx.
 * TODO: it might be more performant to use a dedicated objet for the heap's
 * items.
 *
 * @param  {Graph}  graph         - The graphology instance.
 * @param  {array}  sources       - A list of sources.
 * @param  {string} getEdgeWeight - Name of the weight attribute or getter function.
 * @param  {number} cutoff        - Maximum depth of the search.
 * @param  {string} target        - Optional target to reach.
 * @param  {object} paths         - Optional paths object to maintain.
 * @return {object}               - Returns the paths.
 */
function abstractDijkstraMultisource(
  graph,
  sources,
  getEdgeWeight,
  cutoff,
  target,
  paths
) {
  if (!isGraph(graph))
    throw new Error(
      'graphology-shortest-path/dijkstra: invalid graphology instance.'
    );

  if (target && !graph.hasNode(target))
    throw new Error(
      'graphology-shortest-path/dijkstra: the "' +
        target +
        '" target node does not exist in the given graph.'
    );

  getEdgeWeight = createEdgeWeightGetter(
    getEdgeWeight || DEFAULT_WEIGHT_ATTRIBUTE
  ).fromMinimalEntry;

  var distances = {},
    seen = {},
    fringe = new Heap(DIJKSTRA_HEAP_COMPARATOR);

  var count = 0,
    edges,
    item,
    cost,
    v,
    u,
    e,
    d,
    i,
    j,
    l,
    m;

  for (i = 0, l = sources.length; i < l; i++) {
    v = sources[i];
    seen[v] = 0;
    fringe.push([0, count++, v]);

    if (paths) paths[v] = [v];
  }

  while (fringe.size) {
    item = fringe.pop();
    d = item[0];
    v = item[2];

    if (v in distances) continue;

    distances[v] = d;

    if (v === target) break;

    edges = graph.outboundEdges(v);

    for (j = 0, m = edges.length; j < m; j++) {
      e = edges[j];
      u = graph.opposite(v, e);
      cost = getEdgeWeight(e, graph.getEdgeAttributes(e)) + distances[v];

      if (cutoff && cost > cutoff) continue;

      if (u in distances && cost < distances[u]) {
        throw Error(
          'graphology-shortest-path/dijkstra: contradictory paths found. Do some of your edges have a negative weight?'
        );
      } else if (!(u in seen) || cost < seen[u]) {
        seen[u] = cost;
        fringe.push([cost, count++, u]);

        if (paths) paths[u] = paths[v].concat(u);
      }
    }
  }

  return distances;
}

/**
 * Single source Dijkstra shortest path between given node & other nodes in
 * the graph.
 *
 * @param  {Graph}  graph         - The graphology instance.
 * @param  {string} source        - Source node.
 * @param  {string} getEdgeWeight - Name of the weight attribute or getter function.
 * @return {object}               - An object of found paths.
 */
function singleSourceDijkstra(graph, source, getEdgeWeight) {
  var paths = {};

  abstractDijkstraMultisource(graph, [source], getEdgeWeight, 0, null, paths);

  return paths;
}

function bidirectionalDijkstra(graph, source, target, getEdgeWeight) {
  return abstractBidirectionalDijkstra(graph, source, target, getEdgeWeight)[1];
}

/**
 * Function using Ulrik Brandes' method to map single source shortest paths
 * from selected node.
 *
 * [Reference]:
 * Ulrik Brandes: A Faster Algorithm for Betweenness Centrality.
 * Journal of Mathematical Sociology 25(2):163-177, 2001.
 *
 * @param  {Graph}  graph         - Target graph.
 * @param  {any}    source        - Source node.
 * @param  {string} getEdgeWeight - Name of the weight attribute or getter function.
 * @return {array}                - [Stack, Paths, Sigma]
 */
function brandes(graph, source, getEdgeWeight) {
  source = '' + source;

  getEdgeWeight = createEdgeWeightGetter(
    getEdgeWeight || DEFAULT_WEIGHT_ATTRIBUTE
  ).fromMinimalEntry;

  var S = [],
    P = {},
    sigma = {};

  var nodes = graph.nodes(),
    edges,
    item,
    pred,
    dist,
    cost,
    v,
    w,
    e,
    i,
    l;

  for (i = 0, l = nodes.length; i < l; i++) {
    v = nodes[i];
    P[v] = [];
    sigma[v] = 0;
  }

  var D = {};

  sigma[source] = 1;

  var seen = {};
  seen[source] = 0;

  var count = 0;

  var Q = new Heap(BRANDES_DIJKSTRA_HEAP_COMPARATOR);
  Q.push([0, count++, source, source]);

  while (Q.size) {
    item = Q.pop();
    dist = item[0];
    pred = item[2];
    v = item[3];

    if (v in D) continue;

    sigma[v] += sigma[pred];
    S.push(v);
    D[v] = dist;

    edges = graph.outboundEdges(v);

    for (i = 0, l = edges.length; i < l; i++) {
      e = edges[i];
      w = graph.opposite(v, e);
      cost = dist + getEdgeWeight(e, graph.getEdgeAttributes(e));

      if (!(w in D) && (!(w in seen) || cost < seen[w])) {
        seen[w] = cost;
        Q.push([cost, count++, v, w]);
        sigma[w] = 0;
        P[w] = [v];
      } else if (cost === seen[w]) {
        sigma[w] += sigma[v];
        P[w].push(v);
      }
    }
  }

  return [S, P, sigma];
}

/**
 * Exporting.
 */
exports.bidirectional = bidirectionalDijkstra;
exports.singleSource = singleSourceDijkstra;
exports.brandes = brandes;
